package roborally;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of robots in which the x-coordinate, the y-coordinate, the directions and
 * 		the amount of energy stored in the robot can be altered.
 * 
 * @author Thomas De Wachter en Tijs Delvaux
 * 
 * @version 1.i
 *
 * @invar	The amount of energy of each robot must be a valid amount of energy for a
 *         	robot which cannot exceed the maximum amount of energy.
 * 			| isValidEnergy(getEnergy(), getMaxEnergy())
 */
public class Robot implements IRobot{

	private long xPos;
	
	private long yPos;
	
	private double energy;
	
	private Direction direction;
	
	private final double maxEnergy;
	
	private static double CONSUME_TURN = 100;
	private static double CONSUME_MOVE = 500;
	
	/**
	 * 
	 * @param xPos
	 * 			The robot's position along the x-axis
	 * @param yPos
	 * 			The robot's position along the y-axis
	 * @param direction
	 * 			The direction the robot is facing
	 * @param energy
	 * 			The amount of energy stored by the robot
	 * @throws 	IllegalArgumentException
	 * 			| ! isValidEnergy(energy,getMaxEnergy())
	 * @effect	The robot will be put at position (xPos, yPos) facing UP (0), RIGHT (1), DOWN (2) or LEFT (3)
	 * 				with an amount energy stored.
	 * 			| setxPos(xPos);
	 *			| setyPos(yPos);
	 *			| setDirection(direction);
	 *			| setEnergy(energy);
	 */
	public Robot(long xPos, long yPos, int direction, double energy) throws IllegalArgumentException{
		this.maxEnergy = 20000;
		
		try{
			setxPos(xPos);
			setyPos(yPos);
		} catch(IllegalArgumentException exc){
			throw exc;
		}
		setDirection(Direction.values()[direction]);
		
		if(isValidEnergy(energy,getMaxEnergy())){
			setEnergy(energy);
		}else{
			throw new IllegalArgumentException();
		}
	}
	
	/**
	 * 
	 * @return the x-coordinate at which the robot currently is located at.
	 */
	@Basic
	public long getxPos() {
		return xPos;
	}

	/**
	 * 
	 * @param xPos
	 * @post	The x-coordinate of the robot will be changed into xPos.
	 * 			| new.getxPos() == xPos			
	 * @throws	IllegalArgumentException
	 * 			When xPos is negative or above the maximum value (in this case Long.MAX_VALUE)
	 * 				an IllegalArgumentException will be thrown.
	 * 			| (xPos < 0) || (xPos > Long.MAX_VALUE)
	 */
	public void setxPos(long xPos) throws IllegalArgumentException{
		if ((xPos < 0) || (xPos > Long.MAX_VALUE))
			throw new IllegalArgumentException();
		this.xPos = xPos;
	}
	
	/**
	 * 
	 * @return the y-coordinate at which the robot currently is located at.
	 */
	@Basic
	public long getyPos() {
		return yPos;
	}

	/**
	 * 
	 * @param yPos
	 * @post	The y-coordinate of the robot will be changed into xPos.
	 * 			| new.getyPos() == yPos
	 * @throws	IllegalArgumentException
	 * 			When yPos is negative or above the maximum value (in this case Long.MAX_VALUE)
	 * 				an IllegalArgumentException will be thrown.
	 * 			| (yPos < 0)  || (yPos > Long.MAX_VALUE)
	 */
	public void setyPos(long yPos) throws IllegalArgumentException{
		if ((yPos < 0) || (yPos > Long.MAX_VALUE))
			throw new IllegalArgumentException();
		this.yPos = yPos;
	}

	/**
	 * 
	 * @return the current amount of energy stored by the robot.
	 */
	@Basic
	public double getEnergy() {
		return energy;
	}
	/**
	 * 
	 * @return the maximum amount of energy, maxEnergy, a robot can store.
	 */
	@Basic
	public double getMaxEnergy(){
		return maxEnergy;
	}
	
	/**
	 * 
	 * @param energy
	 * @return	True if the energy is within the boundaries of 0 and maxEnergy.	
	 * 			| result == (energy >= 0) && (energy <= maxEnergy)
	 */
	public static boolean isValidEnergy(double energy, double maxEnergy){
		return (energy >= 0) && (energy <= maxEnergy);
	}
	
	/**
	 * @pre		energy must be a valid amount of energy.
	 *			| isValidEnergy(energy, getMaxEnergy()) 
	 * @param energy
	 * @post	energy will be the new amount of energy store in the robot.
	 * 			| new.getEnergy() == energy
	 */
	public void setEnergy(double energy) {
		if(isValidEnergy(energy, getMaxEnergy())){
			this.energy = energy;
		}
	}
	
	
	/**
	 * 
	 * @return	The fraction of the current amount of energy and 
	 * 				the maximum amount of energy.
	 * 			| result == getEnergy()/getMaxEnergy()
	 */
	public double getEnergyFraction(){
		return getEnergy()/getMaxEnergy();
	}
	
	/**
	 * 
	 * @return	True is the robot has enough energy to move.
	 * 			| result == (getEnergy() >= CONSUME_MOVE)
	 */
	public boolean hasMoveEnergy(){
		return getEnergy() >= CONSUME_MOVE;
	}
	
	/**
	 * 
	 * @return	True is the robot has enough energy to turn.
	 * 			| (getEnergy() >= CONSUME_TURN)
	 */
	public boolean hasTurnEnergy(){
		return getEnergy() >= CONSUME_TURN;
	}
	
	/**
	 * 
	 * @pre 	Check whether the energy after recharging is a legal amount of energy.
	 * 			| isValidEnergy(this.getEnergy()+amount, getMaxEnergy())
	 * @param 	amount
	 * @post 	The energy will be charged with amount.
	 * 				If the sum of the current energy and amount is higher than the maximum possible energy,
	 * 				then the energy will be set to the maximum energy.	
	 * 			| new.getEnergy() == Math.max(this.getEnergy() + amount, getMaxEnergy())
	 */
	public void recharge(double amount){
		assert isValidEnergy(this.getEnergy()+amount, getMaxEnergy());
		setEnergy(Math.min(this.getEnergy() + amount, getMaxEnergy()));
	}
	
	/**
	 * 
	 * @pre 	Check whether the energy after consuming is a legal amount of energy.
	 * 			| isValidEnergy(this.getEnergy()-amount, getMaxEnergy())
	 * @param 	amount
	 * @post 	The energy will decreased with amount.
	 * 				If the sum of the current energy and amount is lower than 0,
	 * 				the energy will be set to the 0.		
	 * 			| new.getEnergy() == Math.max(this.getEnergy() - amount, 0)
	 */
	public void consume(double amount){
		assert isValidEnergy(this.getEnergy()-amount, getMaxEnergy());
		setEnergy(Math.max(this.getEnergy() - amount, 0));
	}
	
	/**
	 * 
	 * @return	The direction at which the robot is facing.
	 */
	@Basic
	public Direction getDirection() {
		return direction;
	}
	
	
	/**
	 * 
	 * @return 	The direction at which the robot is facing as an integer.
	 * 				UP (0), RIGHT (1), DOWN (2) or LEFT (3).
	 */
	public int getDirectionNumber(){
		return getDirection().getNumber();
	}
	
	/**
	 * 
	 * @param direction
	 * @post	Change the direction of the robot to the given direction.
	 * 			| new.getDirection() == direction
	 */
	public void setDirection(Direction direction) {
		this.direction = direction;
	}
	
	/**
	 * @effect	Turns the robot clockwise.
	 * 			| new.getDirection() == this.getDirection().getNext()
	 * @effect	When turning, the robot consumes an amount of CONSUME_TURN energy.
	 * 			| consume(CONSUME_TURN);
	 */
	public void turnClockwise(){
		if(hasTurnEnergy()){
			consume(CONSUME_TURN);
			setDirection(getDirection().getNext());
		}
	}
	
	/**
	 * @effect	Turns the robot counterclockwise.
	 * 			| new.getDirection() == this.getDirection().getPrevious() 
	 * @effect	When turning, the robot consumes an amount of CONSUME_TURN energy.
	 * 			| consume(CONSUME_TURN);
	 */
	public void turnCounterClockwise(){
		if(hasTurnEnergy()){
			consume(CONSUME_TURN);
			setDirection(getDirection().getPrevious());
		}
	}
	
	/**
	 *
	 * @effect	When moving, the robot consumes an amount of CONSUME_MOVE energy.
	 * 			| consume(CONSUME_MOVE);
	 * @effect 	For every possible direction, move the robot towards that direction.
	 * 				Facing:	UP, the y-coordinate will be subtracted with 1.
	 * 						RIGHT, the x-coordinate will be increased with 1.
	 * 						DOWN, the y-coordinate will be increased with 1.
	 * 						LEFT, the x-coordinate will be subtracted with 1.
	 * 			| switch(getDirection()){
	 *			| case UP:
	 *			| 	setyPos(getyPos() - 1);
	 *			| case RIGHT:
	 *			| 	setxPos(getxPos() + 1);
	 *			| case DOWN:
	 *			| 	setyPos(getyPos() + 1);
	 *			| case LEFT:
	 *			| 	setxPos(getxPos() - 1);
	 */
	public void move(){
		if(hasMoveEnergy()){
			try{
				switch(getDirection()){
				case UP:
					setyPos(getyPos() - 1);
					break;
				case RIGHT:
					setxPos(getxPos() + 1);
					break;
				case DOWN:
					setyPos(getyPos() + 1);
					break;
				case LEFT:
					setxPos(getxPos() - 1);
					break;
				default: assert false; // impossible to come here
				}
			} catch(IllegalArgumentException exc){
				return;
			}
			consume(CONSUME_MOVE);
		}
	}
	
	/**
	 * 
	 * @param x
	 * @param y
	 * @return The manhattan distance between the current position of the robot
	 * 			and the position (x, y)
	 */
	public long getDistanceTo(long x, long y){
		return Math.abs(y-getyPos())+Math.abs(x-getxPos());
	}
	
	/**
	 * @pre 	The x must be within the borders of the board, which are 0 and Long.MAX_VALUE.
	 * 			| (x >= 0) && (x <= Long.MAX_VALUE)
	 * @pre 	The y must be within the borders of the board, which are 0 and Long.MAX_VALUE.
	 * 			| (y >= 0) && (y <= Long.MAX_VALUE)
	 * @param x
	 * @param y
	 * @param withTurn - whether the energy cost of turning must be taken into account
	 * @return	The amount of energy the robot consumes to go from its current position to (x, y).
	 * 				The robot consumer CONSUME_MOVE when it has to move.
	 * 				Only when the parameter withTurn is true, the robot consumes CONSUME_TURN when it has to turn.
	 */
	public double getEnergyRequiredToReach(long x, long y, boolean withTurn){
		assert (x >= 0) && (x <= Long.MAX_VALUE) && (y >= 0) && (y <= Long.MAX_VALUE);
		if(withTurn){
			double turningEnergy = 0;
			switch(getDirection()){
			case UP: 
				if(y>getyPos()){
					turningEnergy = 2*CONSUME_TURN;
				}else{
					if(x==getxPos()){
						turningEnergy = 0;
					}else{
						turningEnergy = CONSUME_TURN;
					}
				}
				break;
			case RIGHT:
				if(x<getxPos()){
					turningEnergy = 2*CONSUME_TURN;
				}else{
					if(y==getyPos()){
						turningEnergy = 0;
					}else{
						turningEnergy = CONSUME_TURN;
					}
				}
				break;
			case DOWN: 
				if(y<getyPos()){
					turningEnergy = 2*CONSUME_TURN;
				}else{
					if(x==getxPos()){
						turningEnergy = 0;
					}else{
						turningEnergy = CONSUME_TURN;
					}
				}
				break;
			case LEFT: 
				if(x>getxPos()){
					turningEnergy = 2*CONSUME_TURN;
				}else{
					if(y==getyPos()){
						turningEnergy = 0;
					}else{
						turningEnergy = CONSUME_TURN;
					}
				}
				break;
			default: assert false; // impossible to come here
			}
			return getDistanceTo(x, y)*CONSUME_MOVE + turningEnergy;
		}
		else{
			return getDistanceTo(x, y)*CONSUME_MOVE;
		}
	}
	
	/**
	 * @param x
	 * @param y
	 * @param withTurn - whether the energy cost of turning must be taken into account
	 */
	public void moveNextTo(long x, long y,  boolean withTurn){
		
		if(! hasMoveEnergy()){
			return;
		}
		if(getDistanceTo(x, y) <= 1){
			if(getDistanceTo(x, y) == 0){
				if(getxPos() == 0){
					if(getDirection().equals(Direction.UP)){
						turnClockwise();
					}
				}else if( getyPos() == 0){
					if(getDirection().equals(Direction.LEFT)){
						turnCounterClockwise();
					}
				}
				move();
			}else{
				return;
			}
		}else{
			if (! withTurn){
				switch(getDirection()){
				case UP:
					if(y<getyPos()){
						move();
					}else{
						if(x>getxPos()){
							turnClockwise();
						}else{
							turnCounterClockwise();
						}
					}
					break;
				case RIGHT:
					if(x>getxPos()){
						move();
					}else{
						if(y>getyPos()){
							turnClockwise();
						}else{
							turnCounterClockwise();
						}
					}
					break;
				case DOWN:
					if(y>getyPos()){
						move();
					}else{
						if(x<getxPos()){
							turnClockwise();
						}else{
							turnCounterClockwise();
						}
					}
					break;
				case LEFT:
					if(x<getxPos()){
						move();
					}else{
						if(y<getyPos()){
							turnClockwise();
						}else{
							turnCounterClockwise();
						}
					}
					break;
				}
				moveNextTo(x, y, false);
			}else{
				if(getPriority(x, y) == 0){
					move();
				}else if(getPriority(x, y) == 1){
					IRobot clone = this.clone();
					clone.turnClockwise();
					if(clone.getPriority(x, y) == 0){
						this.turnClockwise();
					}else{
						this.turnCounterClockwise();
					}
				}else{
					turnClockwise();
				}
			}
		}
	}
	
	/**
	 * 
	 */
	public double getPriority(long goToX, long goToY){
		if(getDistanceTo(goToX, goToY)==1){
			return -1;
		}
		double priority = 0;
		switch(getDirection()){
		case UP: 
			if(goToY<getyPos()){
				priority = 0;
			}else{
				if(goToX==getxPos()){
					priority = 2;
				}else{
					priority = 1;
				}
			}
			break;
		case RIGHT:
			if(goToX>getxPos()){
				priority = 0;
			}else{
				if(goToY==getyPos()){
					priority = 2;
				}else{
					priority = 1;
				}
			}
			break;
		case DOWN: 
			if(goToY>getyPos()){
				priority = 0;
			}else{
				if(goToX==getxPos()){
					priority = 2;
				}else{
					priority = 1;
				}
			}
			break;
		case LEFT: 
			if(goToX<getxPos()){
				priority = 0;
			}else{
				if(goToY==getyPos()){
					priority = 2;
				}else{
					priority = 1;
				}
			}
			break;
		default: assert false; // impossible to come here
		}
		return priority;
	}
	
	/**
	 * 
	 */
	public IRobot clone(){
		IRobot clone = new Robot(this.getxPos(), this.getyPos(), this.getDirectionNumber(), this.getEnergy());
		return clone;
	}
	
}
